using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Web;
using Conusic.Properties;

namespace Conusic
{
    public static class VkontakteWallPost
    {
        #region Regular Expression define

        private const string PostDataTemplateProfile =
            @"act=post&al=1&hash={0}&message={1}&note_title=&official=&status_export=&to_id={2}&type=all";

        private const string PostDataTemplatePostMedia =
            "act=post&al=1&hash={2}&media={3}&media_type=graffiti&message={1}&type=all&to_id={0}";

        private const string PostDataTemplatePostMediaOldWall =
            "act=post&al=1&hash={2}&media={3}&media_type=graffiti&message={1}&type=all&to_id=-{0}&official={4}";

        private const string PostDataTemplateGroup = @"act=a_post_wall&hash={0}&message={1}&to_id=-{2}&type=1";
        private const string PostDataTemplateEvent = @"act=a_post_wall&hash={0}&message={1}&to_id=-{2}";
        private const string RegexParsePostHashProfile = @"""post_hash"":""([0-9a-f]*)""";
        private const string RegexParsePostHashEvent = @"act: 'a_post_wall', hash: decodehash\('([a-f0-9]*)'";
        private const string RegexParsePostHashGroup = @"postWall\([^,]*, '([a-f0-9]*)'\)";
        private const string RegexParseGraffitiHashGroup = @"""post_hash"":""([0-9a-f]*)""";
        private const string RegexParseLastMediaId = "<!>([0-9]*_[0-9]*)<!>";
        private const int CorrectPageMinimalLenght = 1024;

        private const string PostDataTemplateUploadGraffity =
            "--{0}\nContent-Disposition: form-data; name=\"Signature\"\n\n{1}\n--{0}\nContent-Disposition: form-data; name=\"Filedata\"; filename=\"graffiti.png\"\nContent-Type: image/png\n\n";

        private const string PostDataTemplateUploadGraffityHtml5 =
            "--{0}\r\nContent-Disposition: form-data; name=\"canvas\"; filename=\"graffiti.png\"\r\n" +
            "Content-Type: image/png\r\n\r\n" +
            "{1}\r\n--{0}--\r\n";

        private const string Boundary = "----------------------GRAFFITI";

        private const string ContentTypeGraffiti = "multipart/form-data; boundary={0}";
        private const string RequestMediaIdPostData = "act=last_graffiti&al=1";

        private const string PostDataTemplatePostWithMedia =
            "act=post&al=1&attach1={3}&attach1_type=graffiti&facebook_export=&friends_only=&hash={2}&message={1}&note_title=&official={4}&status_export=&to_id={0}&type={5}";

        #endregion

        /// <summary>
        /// Vkonate use obfuscate code to safe from spam.
        /// Use firebug to solve this problem. See genhash.html in doc folder
        /// this algorithm based on this file.
        /// </summary>
        /// <param name="hash">The hash to convert.</param>
        /// <returns>The hash to post format.</returns>
        internal static string ConvertHashToPostFormat(string hash)
        {
            string firstFunctionExecution = hash.Substring(hash.Length - 5, 5) + hash.Substring(4, hash.Length - 12);
            var toReverse = firstFunctionExecution.ToCharArray();
            Array.Reverse(toReverse);
            string result = new string(toReverse);
            return result;
        }

        /// <summary>
        /// New function for generate signature for HTML5 graffiti sender
        /// </summary>
        /// <param name="base64Image">Image encoded in base64 format</param>
        /// <returns>Signature of image</returns>
        internal static string GetImageSignature(string base64Image, int offset)
        {
            var sign = "";
            int len = base64Image.Length - 1;
            int step = (int) Math.Floor((double) len/8);
            for (int i = len; i > 0; i -= step)
            {
                sign = string.Format("{0}{1:X}", sign, (base64Image[i] + offset)%16);
            }
            return sign.ToLower(CultureInfo.InvariantCulture);
        }

        /// <summary>
        /// Deprecated
        /// Algorithm to generate signature is md5(base64(substr(file,0,1024))).
        /// </summary>
        /// <param name="file">Bytes of files, nedd to be converted.</param>
        /// <returns>Signature string.</returns>
        private static string GenerateSignatureGraffity(byte[] file)
        {
            ////768 enoght because we use base64
            const int BytesCount = 768;

            byte[] toEncodeAsBytes = new byte[BytesCount];
            Array.Copy(file, toEncodeAsBytes, Math.Min(file.Length, BytesCount));

            string ha = Convert.ToBase64String(toEncodeAsBytes);
            return GetMd5Hash(ha);
        }

        private static string GetMd5Hash(string input)
        {
            MD5 md5Hasher = MD5.Create();
            byte[] data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(input));

            StringBuilder sBuilder = new StringBuilder();

            for (int i = 0; i < data.Length; i++)
            {
                sBuilder.Append(data[i].ToString("x2"));
            }

            return sBuilder.ToString();
        }

        #region Parsing Functions

        private static string ParsePostHashProfile(string page)
        {
            var regex = new Regex(RegexParsePostHashProfile);
            var match = regex.Match(page);
            if (match.Success)
            {
                return match.Groups[1].ToString();
            }
            else
            {
                return null;
            }
        }

        private static string ParsePostHashEvent(string page)
        {
            var regex = new Regex(RegexParsePostHashEvent);
            var match = regex.Match(page);
            if (match.Success)
            {
                return ConvertHashToPostFormat(match.Groups[1].ToString());
            }
            else
            {
                return null;
            }
        }

        private static string ParsePostHashGroup(string page)
        {
            var regex = new Regex(RegexParsePostHashGroup);
            var match = regex.Match(page);
            if (match.Success)
            {
                return ConvertHashToPostFormat(match.Groups[1].ToString());
            }
            if ((match = Regex.Match(page, RegexParsePostHashEvent)).Success)
            {
                return ConvertHashToPostFormat(match.Groups[1].ToString());
            }
            return null;
        }

        private static string ParseGraffitiHashGroup(string page)
        {
            var match = Regex.Match(page, RegexParseGraffitiHashGroup);
            if (match.Success)
            {
                return match.Groups[1].ToString();
            }
            return null;
        }

        private static string ParseLastMediaId(string page)
        {
            var regex = new Regex(RegexParseLastMediaId);
            var match = regex.Match(page);
            if (match.Success)
            {
                return match.Groups[1].ToString();
            }
            return null;
        }


        private static string ParseGraffitiId(string page)
        {
            var regex = new Regex("done<\\*>([0-9]*_[0-9]*)");
            var match = regex.Match(page);
            if (match.Success)
            {
                return match.Groups[1].ToString();
            }
            return null;
        }

        private static string ParsePostHash(string page)
        {
            var match = Regex.Match(page, "&hash=([a-f0-9]+)");
            if (match.Success)
            {
                return match.Groups[1].ToString();
            }
            return null;
        }

        private static int ParseOffset(string page)
        {
            var match = Regex.Match(page, "return __\\+([0-9]+)");
            if (match.Success)
            {
                return int.Parse(match.Groups[1].ToString());
            }
            throw new ParseException("offset parse exception");
        }

        #endregion

        internal static string RequestPostHash(Profile id, ref bool canBeOfficial)
        {
            string page = SimpleHttp.RequestVkontaktePage(id.Url);
            string result = null;
            switch (id.ProfileType)
            {
                case ProfileTypes.Profile:
                    result = ParsePostHashProfile(page);
                    break;
                case ProfileTypes.Group:
                    result = ParseGraffitiHashGroup(page);
                    canBeOfficial = page.Contains(@"id=""official""");
                    break;
                case ProfileTypes.Event:
                    result = ParseGraffitiHashGroup(page);
                    break;
                case ProfileTypes.PublicPage:
                    result = ParseGraffitiHashGroup(page);
                    break;
            }

            if (String.IsNullOrEmpty(result))
            {
                throw new ParseHashFailException(page);
            }
            return result;
        }

        private static string RequestLastMediaId()
        {
            string page = SimpleHttp.RequestVkontaktePage("al_wall.php", RequestMediaIdPostData);
            string result = ParseLastMediaId(page);

            if (String.IsNullOrEmpty(result))
            {
                throw new ParseMediaIdFailException(page);
            }
            return result;
        }

        public static string RequestGraffitiHash(Profile id, ref bool canBeOfficial, out int offset)
        {
            string page = SimpleHttp.RequestVkontaktePage(
                "al_wall.php",
                String.Format("act=canvas_draw_box&al=1&flash=10&to_id={0}", id.Oid)
                );

            string result = null;
            result = ParsePostHash(page);
            offset = ParseOffset(page);

            if (String.IsNullOrEmpty(result))
            {
                throw new ParseHashFailException(page);
            }
            return result;
        }

        private static bool CheckCorrectAnswerForSendMessage(string page)
        {
            //// If server send answer page (ok) - we have a big page => page.Lenght > 1024
            //// native checked is split page for <!> - check count, and see to zero is 4 position.
            if (page.Length > CorrectPageMinimalLenght)
            {
                return true;
            }
            return false;
        }

        private static string UploadHtml5Graffiti(Profile id, byte[] file, string hash, int offset)
        {
            //// Wait for Vkontakte access tube
            Config.Instance.VkontakteAccessTube.Wait();

            string base64Image = Convert.ToBase64String(file);
            string signature = GetImageSignature(base64Image, offset);
            string url = String.Format("{0}graffiti.php?act=save_canvas&oid={1}&hash={2}&sign={3}",
                                       Settings.Default.VkontakteAddress,
                                       id.Oid,
                                       hash,
                                       signature
                );

            var request = (HttpWebRequest) WebRequest.Create(url);
            
            // parametrize proxy configuration
            Config.Instance.SetProxyConfiguration(request);
            Config.Instance.SetVkontakteCookieForWebRequest(request);

            string postData = String.Format(
                PostDataTemplateUploadGraffityHtml5,
                Boundary,
                base64Image);
            byte[] byteArray = Encoding.ASCII.GetBytes(postData);

            request.Method = "POST";
            request.ContentType = String.Format(ContentTypeGraffiti, Boundary);
            request.ContentLength = byteArray.Length;

            Stream outputStream = request.GetRequestStream();
            outputStream.Write(byteArray, 0, byteArray.Length);
            outputStream.Close();

            var response = request.GetResponse();
            var stream = new WebResponseCp1251Reader(response);
            string page = stream.ReadToEndAndCloseAll();

            string result = ParseGraffitiId(page);
            if (string.IsNullOrEmpty(result))
                throw new GraffitiUploadException();
            return result;
        }

        private static void UploadGraffiti(Profile id, byte[] file)
        {
            const int additionalSize = 4;

            //// Wait for Vkontakte access tube
            Config.Instance.VkontakteAccessTube.Wait();

            string url = String.Format(
                "{0}graffiti.php?to_id={1}&group_id={2}",
                Settings.Default.VkontakteAddress,
                id.ProfileType == ProfileTypes.Profile ? id.Id : String.Empty,
                id.ProfileType == ProfileTypes.Group ? id.Id : String.Empty);
            var request = (HttpWebRequest) WebRequest.Create(url);
            
            // parametrize proxy configuration
            Config.Instance.SetProxyConfiguration(request);
            Config.Instance.SetVkontakteCookieForWebRequest(request);


            string postData = String.Format(
                PostDataTemplateUploadGraffity,
                Boundary,
                GenerateSignatureGraffity(file));

            byte[] byteArray = Encoding.ASCII.GetBytes(postData);

            request.Method = "POST";
            request.ContentType = String.Format(ContentTypeGraffiti, Boundary);
            request.ContentLength = byteArray.Length + file.Length + Boundary.Length + additionalSize;

            Stream outputStream = request.GetRequestStream();
            outputStream.Write(byteArray, 0, byteArray.Length);
            outputStream.Write(file, 0, file.Length);
            outputStream.Write(Encoding.ASCII.GetBytes("\n--" + Boundary + "\n"), 0, Boundary.Length + additionalSize);
            outputStream.Close();

            var response = request.GetResponse();
            var stream = new WebResponseCp1251Reader(response);
            string page = stream.ReadToEndAndCloseAll();
        }
   
        private static void PostMessageWithMedia(Profile id, string message, string hash, string mediaId,
                                                 Captcha capthcaHandler, bool official)
        {
            // Wait for Vkontakte access tube
            Config.Instance.VkontakteAccessTube.Wait();

            string url = "al_wall.php";

            string postData =
                String.Format(
                    PostDataTemplatePostWithMedia,
                    id.Oid,
                    HttpUtility.UrlEncode(message),
                    hash,
                    mediaId,
                    official ? '1':'0',
                    id.ProfileType == ProfileTypes.PublicPage ? "own" : "all");


            string page = SimpleHttp.RequestVkontaktePage(url, postData);
            var parsedAnswer = page.Split((new[] {"<!>"}), StringSplitOptions.None);

            // Check captcha need for group and events pages
            // !IMPORTANT - NOT USED IN GROUPS TODO: REMOVE
            if (capthcaHandler != null && capthcaHandler.CheckNeedCaptcha(page))
            {
                postData += capthcaHandler.RequestCaptchaAndGetPostParams(page);
                page = SimpleHttp.RequestVkontaktePage(url, postData);

                if (capthcaHandler.CheckNeedCaptcha(page))
                {
                    throw new CaptchaException();
                }
            }
                // Answer 2 - is capctcha needed(common.js:2109) (for al_wall pages)
            else if (capthcaHandler != null && parsedAnswer[4] == "2")
            {
                capthcaHandler.sid = parsedAnswer[5];
                postData += String.Format("{0}&captcha_sid={1}&captcha_key={2}", postData, capthcaHandler.sid,
                                          capthcaHandler.RequestEnterCaptcha());
                page = SimpleHttp.RequestVkontaktePage(url, postData);
                parsedAnswer = page.Split((new[] {"<!>"}), StringSplitOptions.None);

                if (parsedAnswer[4] == "2")
                {
                    throw new CaptchaException();
                }
            }
        }

        private static void SafeSetProgress(IPostGraffitiCommand command, int percent)
        {
            if (command != null)
            {
                command.Progress(percent);
            }
        }

        public static void PostGraffitiAsync(Profile id, string message, byte[] file, IPostGraffitiCommand command)
        {
            ThreadStart threadStart = delegate { PostGraffiti(id, message, file, command); };
            var thread = new Thread(threadStart);
            thread.Start();
        }

        public static void PostGraffiti(Profile id, string message, byte[] file)
        {
            PostGraffiti(id, message, file, null);
        }

        /// <summary>
        /// Main method Post Graffiti. Can called from PostGraffiti(Profile, message, byte[]) and PostGraffitiAsync
        /// Never return error if command not null. 
        /// </summary>
        /// <param name="id">Profile where send graffiti.</param>
        /// <param name="message">Text message.</param>
        /// <param name="file">Png-file with graffiti picture.</param>
        /// <param name="command">IPostGraffitiCommand class for callback.</param>
        private static void PostGraffiti(Profile id, string message, byte[] file, IPostGraffitiCommand command)
        {
            try
            {
                bool canBeOfficial = false;

                SafeSetProgress(command, 0);
                int offsetSignGen;

                // Step 1(Graffii Post Process Description)
                string graffitiHash = RequestGraffitiHash(id, ref canBeOfficial, out offsetSignGen);
                SafeSetProgress(command, 20);

                string hash = RequestPostHash(id, ref canBeOfficial);

                // Step 2(Graffii Post Process Description)
                string mediaId = UploadHtml5Graffiti(id, file, graffitiHash, offsetSignGen);
                SafeSetProgress(command, 60);

                // Step 4(Graffii Post Process Description)
                PostMessageWithMedia(
                    id,
                    message,
                    hash,
                    mediaId,
                    command == null ? null : command.CaptchaHandler,
                    canBeOfficial && command != null && command.OfficialCheck());

                SafeSetProgress(command, 100);
                if (command != null)
                {
                    command.Execute(id);
                }
            }
            catch (Exception e)
            {
                if (command != null)
                {
                    command.Fail(e);
                }
                else
                {
                    throw;
                }
            }
        }
    }
}